﻿using Common;
using Linguist.Acoustic.Tiedstate;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Linguist.Acoustic.Tiedstate
{
    /// <summary>
    /// Used for clustering gaussians. The clustering is performed by Euclidean
    /// distance criterion. The "k-means" clustering algorithm is used for clustering
    /// the gaussians.
    /// 
    /// @author Bogdan Petcu
    /// </summary>
    public class ClusteredDensityFileData : IClusteredDensityFileData
    {
        private int numberOfClusters;
        private int[] corespondingClass;

        public ClusteredDensityFileData(ILoader loader, int numberOfClusters)
        {
            this.numberOfClusters = numberOfClusters;
            kMeansClustering(loader, 30);
        }

        public int getNumberOfClusters()
        {
            return this.numberOfClusters;
        }

        /**
        /// Used for accessing the index that is specific to a gaussian.
        /// 
        /// @param gaussian
        ///            provided in a i/// numStates + gaussianIndex form.
        /// @return
         */
        public int getClassIndex(int gaussian)
        {
            return corespondingClass[gaussian];
        }

        /**
        /// Computes euclidean distance between 2 n-dimensional points.
        /// 
        /// @param a
        ///            - n-dimensional "a" point
        /// @param b
        ///            - n-dimensional "b" point
        /// @return the euclidean distance between a and b.
         */
        private float euclidianDistance(float[] a, float[] b)
        {
            double s = 0, d;

            for (int i = 0; i < a.Length; i++)
            {
                d = a[i] - b[i];
                s += d* d;
            }

            return (float)Math.Sqrt(s);
        }

        /**
        /// Checks if the two float array have the same components
        /// 
        /// @param a
        ///            - float array a
        /// @param b
        ///            - float array b
        /// @return true if values from a are equal to the ones in b, else false.
         */
        private Boolean isEqual(float[] a, float[] b)
        {
            if (a.Length != b.Length)
            {
                return false;
            }

            for (int i = 0; i < a.Length; i++)
            {
                if (a[i] != b[i])
                {
                    return false;
                }
            }

            return true;
        }

        /**
        /// Performs k-means-clustering algorithm for clustering gaussians.
        /// Clustering is done using euclidean distance criterium.
        /// 
        /// @param maxIterations
         */
        private void kMeansClustering(ILoader loader, int maxIterations) 
        {
            Pool<float[]> initialData = loader.getMeansPool();
            List<float[]> oldCentroids = new List<float[]>(numberOfClusters);
            List<float[]> centroids = new List<float[]>(numberOfClusters);

            int numberOfElements = initialData.size(), nrOfIterations = maxIterations, index;
            int[] count = new int[numberOfClusters];
            double distance, min;
            float[] currentValue, centroid;
            float[][][] array = new float[numberOfClusters][][];
            Boolean converged = false;
            Random randomGenerator = new Random();

            for (int i = 0; i < numberOfClusters; i++) 
            {
                index = randomGenerator.Next(numberOfElements);
                centroids.Add(initialData.get(index));
                oldCentroids.Add(initialData.get(index));
                count[i] = 0;
            }

            index = 0;

            while (!converged && nrOfIterations > 0) 
            {
                corespondingClass = new int[initialData.size()];
                array = new float[numberOfClusters][][];

                for (int i = 0; i < numberOfClusters; i++) 
                {
                    oldCentroids[i] = centroids[i];
                    count[i] = 0;
                }

                for (int i = 0; i < initialData.size(); i++) 
                {
                    currentValue = initialData.get(i);
                    min = this.euclidianDistance(oldCentroids[0], currentValue);
                    index = 0;

                    for (int k = 1; k < numberOfClusters; k++) {
                        distance = this.euclidianDistance(oldCentroids[k],
                                currentValue);

                        if (distance < min) {
                            min = distance;
                            index = k;
                        }
                    }

                    array[index][count[index]] = currentValue;
                    corespondingClass[i] = index;
                    count[index]++;

                }

                for (int i = 0; i < numberOfClusters; i++) {
                    centroid = new float[initialData.get(0).Length];

                    if (count[i] > 0) {

                        for (int j = 0; j < count[i]; j++) {
                            for (int k = 0; k < initialData.get(0).Length; k++) 
                            {
                                centroid[k] += array[i][j][k];
                            }
                        }

                        for (int k = 0; k < initialData.get(0).Length; k++) {
                            centroid[k] /= count[i];
                        }

                        centroids[i] = centroid;
                    }
                }

                converged = true;

                for (int i = 0; i < numberOfClusters; i++) {
                    converged = converged
                            && (this.isEqual(centroids[i], oldCentroids[i]));
                }

                nrOfIterations--;
            }
        }

    }
}
